/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.vivo.ic.dm;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import com.vivo.ic.VLog;
import com.vivo.ic.dm.Downloads.Impl;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Pair;

public class DownloadInfo {
    private static final String TAG = Constants.PRE_TAG + "DownloadInfo";
    
    public long mId;
    public String mUri;
    public String mHint;
    public String mFileName;
    public String mMimeType;
    public int mVisibility;
    public int mControl;
    public int mStatus;
    public int mFailedNum = 0;
    public int mRetryAfter;
    public long mLastMod;
    public String mExtras;
    public String mCookies;
    public String mUserAgent;
    public String mReferer;
    public long mTotalBytes;
    public long mCurrentBytes;
    public long mSpeed;
    public String mETag;
    public int mMediaScanned;
    public String mTitle;
    public String mDescription;
    public int mNetworkChanged;
    public int mDownloadType;
    public String mPackageName;
    public boolean mResume = false; // 表示是断点续传的
    
    public String mAppExtraOne;
    public String mAppExtraTwo;
    public String mAppExtraThree;
    public String mAppExtraFour;
    public String mAppExtraFive;
    
    private int mFuzz;
    private final Context mContext;
    private Future<?> mSubmittedTask;
    private List<Pair<String, String>> mRequestHeaders = new ArrayList<Pair<String, String>>();
    
    enum NetworkState {
        // 网络畅通
        OK,
        
        // 无网络
        NO_CONNECTION,
        
        // 出于某些原因（比如禁止后台联网），网络访问被禁
        BLOCKED,
        
        // 移动网络
        MOBILE,
    }
    
    private DownloadInfo(Context context) {
        mContext = context;
        mFuzz = new Random().nextInt(1001);
    }
    
    public Uri getDownloadsUri() {
        return ContentUris.withAppendedId(Impl.CONTENT_URI, mId);
    }
    
    public String getUserAgent() {
        if (mUserAgent != null) {
            return mUserAgent;
        } else {
            return Constants.DEFAULT_USER_AGENT;
        }
    }
    
    public Collection<Pair<String, String>> getHeaders() {
        return Collections.unmodifiableList(mRequestHeaders);
    }
    
    public long restartTime(long now) {
        // 第一次失败，立即重试
        if (mFailedNum == 0) {
            return now;
        }
        // 非第一次失败，且服务器有返回Retry-After字段，则已服务器Retry-After字段为准
        if (mRetryAfter > 0) {
            return mLastMod + mRetryAfter;
        }
        // 以RETRY_FIRST_DELAY和mNumFailed作为参数，计算下一次retry时间
        return mLastMod + Constants.RETRY_FIRST_DELAY * (1000 + mFuzz) * (1 << (mFailedNum - 1));
    }
    
    public long nextActionMillis(long now) {
        // 下载完成了，retry的时间就是无限大的，即永远不尝试
        if (Impl.isStatusCompleted(mStatus)) {
            return Long.MAX_VALUE;
        }
        // 状态不是retry的又没有下载完成的，需要立即进行后续循环
        if (mStatus != Impl.STATUS_WAITING_TO_RETRY) {
            return 0;
        }
        // 状态是retry，这时候就要计算下具体什么时候try了。
        long when = restartTime(now);
        if (when <= now) {
            return 0;
        }
        return when - now;
    }
    
    private boolean isReadyToDownload() {
        VLog.w(TAG, "isReadyToDownload() mStatus: " + mStatus + " mControl: " + mControl);
        if (mControl == Impl.CONTROL_PAUSED) {
            return false;
        }
        if (mStatus == Impl.STATUS_FILE_ERROR) {
            mStatus = Impl.STATUS_RUNNING;
        }
        switch (mStatus) {
        case Impl.STATUS_SUCCESS:
            return false;
        case 0:
        case Impl.STATUS_PENDING:
        case Impl.STATUS_RUNNING:
            return true;
        
        case Impl.STATUS_WAITING_FOR_NETWORK:
        case Impl.STATUS_QUEUED_FOR_WIFI:
            return checkCanUseNetwork() == NetworkState.OK;
        
        case Impl.STATUS_WAITING_TO_RETRY:
            // download was waiting for a delayed restart
            final long now = System.currentTimeMillis();
            return restartTime(now) <= now;
        case Impl.STATUS_DEVICE_NOT_FOUND_ERROR:
            // is the media mounted?
            VLog.w(TAG, "download not ready because of STATUS_DEVICE_NOT_FOUND_ERROR " + mId);
            return false;
        case Impl.STATUS_INSUFFICIENT_SPACE_ERROR:
            // avoids repetition of retrying download
            VLog.w(TAG, "download not ready because of STATUS_INSUFFICIENT_SPACE_ERROR " + mId);
            return false;
        default:
            break;
        }
        VLog.w(TAG, "download not ready because of unknow status " + mStatus);
        return false;
    }
    
    public NetworkState checkCanUseNetwork() {
        final NetworkInfo info = DMUtil.getActiveNetworkInfo();
        NetworkState result;
        if (info == null || !info.isConnected()) {
            result = NetworkState.NO_CONNECTION;
        } else if (DetailedState.BLOCKED.equals(info.getDetailedState())) { /* Access to this network is blocked. */
            result =  NetworkState.BLOCKED;
        } else if (info.getType() == ConnectivityManager.TYPE_MOBILE && !DownloadManager.getConfig().allowUsingMobileNet()) {
            result = NetworkState.MOBILE;
        } else {
            result = NetworkState.OK;
        }
        VLog.d(TAG,"checkCanUseNetwork() result:" + result);
        return result;
    }
    
    public boolean startDownloadIfReady(ExecutorService executor) {
        synchronized (this) {
            final boolean isReady = isReadyToDownload();
            // 通过mSubmittedTask来随时获取线程执行情况，当多下载并发时，超过同时下载数的下载会在队列
            // 中排队而不会被执行。（状态为Pending）
            final boolean isActive = mSubmittedTask != null && !mSubmittedTask.isDone();
            VLog.i(TAG, "startDownloadIfReady() isReady: " + isReady + " isActive: " + isActive);
            if (isReady && !isActive) {
                if (mStatus != Impl.STATUS_RUNNING) {
                    mStatus = Impl.STATUS_RUNNING;
                    ContentValues values = new ContentValues();
                    values.put(Downloads.Column.STATUS, mStatus);
                    mContext.getContentResolver().update(getDownloadsUri(), values, null, null);
                }
                
                Runnable downloadThread = new DownloadThread(mContext, this);
                mSubmittedTask = executor.submit(downloadThread);
            }
            return isReady;
        }
    }
    
    public boolean startScanIfReady(DownloadScanner scanner) {
        synchronized (this) {
            final boolean isReady = shouldScanFile();
            if (isReady) {
                scanner.requestScan(this);
            }
            return isReady;
        }
    }
    
    public boolean shouldScanFile() {
        // mMediaScanned在scan完成的回调中会被置为1，通过对mMediaScanned的校验来防止反复扫描
        return (mMediaScanned == 0) && Impl.isStatusSuccess(mStatus);
    }
    
    /**
     * 数据库中的真实status，每次调用都一定会查询数据库，代价较高，但是数据准确。
     * 
     * @return
     */
    public static int queryDownloadStatus(ContentResolver resolver, long id) {
        final Cursor cursor = resolver.query(ContentUris.withAppendedId(Impl.CONTENT_URI, id),
                new String[] { Downloads.Column.STATUS }, null, null, null);
        try {
            if (cursor.moveToFirst()) {
                return cursor.getInt(0);
            } else {
                // 异常处理，理论上DownloadThread能走到这里就不会出现cursor无数据，
                // 如果数据库中无数据，则相当于此下载已经不需要再进行了。返回SUCCESS阻止thread启动
                VLog.w(TAG, "queryDownloadStatus not in database, return success!!!");
                return Impl.STATUS_SUCCESS;
            }
        } finally {
            cursor.close();
        }
    }
    
    /**
     * 从数据库查询某一个int类型的字段
     * 
     * @return
     */
    public static int queryDownloadColumInt(ContentResolver resolver, long id, String colum) {
        final Cursor cursor = resolver.query(ContentUris.withAppendedId(Impl.CONTENT_URI, id),
                new String[] { colum }, null, null, null);
        try {
            if (cursor.moveToFirst()) {
                return cursor.getInt(0);
            } else {
                VLog.w(TAG, "query colum  not in database, return -1");
                return -1;
            }
        } finally {
            cursor.close();
        }
    }
    
    /**
     * 从更新数据库某一个int类型的字段
     * 
     * @return
     */
    public static int updateDownloadColumInt(ContentResolver resolver, long id, String colum, int value) {
        try {
            ContentValues values = new ContentValues();
            values.put(colum, value);
            return resolver.update(ContentUris.withAppendedId(Impl.CONTENT_URI, id), values, null, null);
        } catch (Exception e) {
            VLog.d(TAG, "updateDownloadColumInt failed");
            return -1;
        }
    }
    
    private ContentValues buildContentValues() {
        final ContentValues values = new ContentValues();
        
        values.put(Downloads.Column.DATA, mFileName);
        values.put(Downloads.Column.MIME_TYPE, mMimeType);
        values.put(Downloads.Column.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter);
        values.put(Downloads.Column.TOTAL_BYTES, mTotalBytes);
        values.put(Downloads.Column.CURRENT_BYTES, mCurrentBytes);
        values.put(Downloads.Column.CURRENT_SPEED, mSpeed);
        values.put(Downloads.Column.STATUS, mStatus);
        values.put(Downloads.Column.LAST_MODIFICATION, System.currentTimeMillis());
        
        return values;
    }
    
    /**
     * Blindly push update of current delta values to provider.
     * @return the number of rows updated.
     */
    public int writeToDatabase(String caller) {
        int rows = mContext.getContentResolver().update(getDownloadsUri(), buildContentValues(), null, null);
        VLog.d(TAG, "writeToDatabase() in: " + caller + " update rows:" + rows);
        return rows;
    }
    
    @Override
    public String toString() {
        return "DownloadInfo [mId=" + mId + ", mFileName=" + mFileName + ", mVisibility=" + mVisibility + ", mControl="
                + mControl + ", mStatus=" + mStatus + ", mSpeed=" + mSpeed + ", mTotalBytes=" + mTotalBytes + ", mCurrentBytes="
                + mCurrentBytes + ", mTitle=" + mTitle + "]";
    }
    
    /**
     * 从数据库到内存缓存（mDownloads）之间的转换桥梁。负责将数据库中的数据同步至缓存中。
     */
    public static class Reader {
        private ContentResolver mResolver;
        private Cursor mCursor;
        
        public Reader(ContentResolver resolver, Cursor cursor) {
            mResolver = resolver;
            mCursor = cursor;
        }
        
        public DownloadInfo newDownloadInfo(Context context) {
            final DownloadInfo info = new DownloadInfo(context);
            updateFromDatabase(info);
            readRequestHeaders(info);
            return info;
        }
        
        public void updateFromDatabase(DownloadInfo info) {
            info.mId = getLong(Downloads.Column._ID);
            info.mUri = getString(Downloads.Column.URI);
            info.mHint = getString(Downloads.Column.FILE_NAME_HINT);
            info.mFileName = getString(Downloads.Column.DATA);
            info.mMimeType = DMUtil.normalizeMimeType(getString(Downloads.Column.MIME_TYPE));
            info.mVisibility = getInt(Downloads.Column.VISIBILITY);
            info.mStatus = getInt(Downloads.Column.STATUS);
            info.mFailedNum = getInt(Downloads.Column.FAILED_CONNECTIONS);
            int retryRedirect = getInt(Downloads.Column.RETRY_AFTER_X_REDIRECT_COUNT);
            info.mRetryAfter = retryRedirect & 0xfffffff;
            info.mLastMod = getLong(Downloads.Column.LAST_MODIFICATION);
            info.mExtras = getString(Downloads.Column.NOTIFICATION_EXTRAS);
            info.mCookies = getString(Downloads.Column.COOKIE_DATA);
            info.mUserAgent = getString(Downloads.Column.USER_AGENT);
            info.mReferer = getString(Downloads.Column.REFERER);
            info.mTotalBytes = getLong(Downloads.Column.TOTAL_BYTES);
            info.mCurrentBytes = getLong(Downloads.Column.CURRENT_BYTES);
            info.mSpeed = getLong(Downloads.Column.CURRENT_SPEED);
            info.mMediaScanned = getInt(Downloads.Column.MEDIA_SCANNED);
            info.mTitle = getString(Downloads.Column.TITLE);
            info.mETag = getString(Downloads.Column.ETAG);
            info.mDescription = getString(Downloads.Column.DESCRIPTION);
            info.mNetworkChanged = getInt(Downloads.Column.NETWORK_CHANGED);
            info.mPackageName = getString(Downloads.Column.PACKAGE_NAME);
            info.mAppExtraOne = getString(Downloads.Column.APP_EXTRA_ONE);
            info.mAppExtraTwo = getString(Downloads.Column.APP_EXTRA_TWO);
            info.mAppExtraThree = getString(Downloads.Column.APP_EXTRA_THREE);
            info.mAppExtraFour = getString(Downloads.Column.APP_EXTRA_FOUR);
            info.mAppExtraFive = getString(Downloads.Column.APP_EXTRA_FIVE);
            
            info.mDownloadType = getInt(Downloads.Column.DOWNLOAD_TYPE);
            
            synchronized (this) {
                info.mControl = getInt(Downloads.Column.CONTROL);
            }
        }
        
        private void readRequestHeaders(DownloadInfo info) {
            info.mRequestHeaders.clear();
            Uri headerUri = Uri.withAppendedPath(info.getDownloadsUri(), Downloads.RequestHeaders.URI_SEGMENT);
            Cursor cursor = mResolver.query(headerUri, null, null, null, null);
            try {
                int headerIndex = cursor.getColumnIndexOrThrow(Downloads.RequestHeaders.COLUMN_HEADER);
                int valueIndex = cursor.getColumnIndexOrThrow(Downloads.RequestHeaders.COLUMN_VALUE);
                for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
                    addHeader(info, cursor.getString(headerIndex), cursor.getString(valueIndex));
                }
            } finally {
                cursor.close();
            }
            
            if (info.mCookies != null) {
                addHeader(info, "Cookie", info.mCookies);
            }
            if (info.mReferer != null) {
                addHeader(info, "Referer", info.mReferer);
            }
        }
        
        private void addHeader(DownloadInfo info, String header, String value) {
            info.mRequestHeaders.add(Pair.create(header, value));
        }
        
        private String getString(String column) {
            int index = mCursor.getColumnIndexOrThrow(column);
            String s = mCursor.getString(index);
            return (TextUtils.isEmpty(s)) ? null : s;
        }
        
        private Integer getInt(String column) {
            return mCursor.getInt(mCursor.getColumnIndexOrThrow(column));
        }
        
        private Long getLong(String column) {
            return mCursor.getLong(mCursor.getColumnIndexOrThrow(column));
        }
    }
}
